/*
 * Copyright 2008 Neil Bacon
 * Distributed under the terms of the Mozilla Public License version 1.1
 * or the GNU General Public License version 3.
 */

package nb.xadd.node.impl;

import gnu.trove.TIntDoubleHashMap;
import gnu.trove.TIntDoubleIterator;
import gnu.trove.TIntDoubleProcedure;

import java.util.Arrays;

import nb.util.Utils;
import nb.xadd.XADDRuntimeException;
import nb.xadd.node.Node;
import nb.xadd.operator.BinaryOperator;

public class PolynomialValueNode extends AbstractValueNode {

    //	private static final Log log = LogFactory.getLog(PolynomialValueNode.class);
    private final int numberOfVariables;
    private final PolynomialCoefficients[] coeff;
    private final int hashCode;

    /**
     * @param numberOfVariables
     * @param coeff coeff[i] holds (i+1)th order coefficients
     * The coeff objects are set immutable so they can be safely shared.
     */
    protected PolynomialValueNode(int numberOfVariables, double constValue, PolynomialCoefficients[] coeff) {
        super(constValue);
        this.numberOfVariables = numberOfVariables;
        this.coeff = coeff;
        for (PolynomialCoefficients c : coeff) {
            c.setImmutable();
        }
        hashCode = calcHashCode();
    }

    @Override
    public int hashCode() {
        return hashCode;
    }

    protected int calcHashCode() {
        final int prime = 31;
        int result = super.calcHashCode();
        result = prime * result + Arrays.hashCode(coeff);
        result = prime * result + numberOfVariables;
        return result;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (!super.equals(obj))
            return false;
        if (getClass() != obj.getClass())
            return false;
        PolynomialValueNode other = (PolynomialValueNode) obj;
        if (!Arrays.equals(coeff, other.coeff))
            return false;
        if (numberOfVariables != other.numberOfVariables)
            return false;
        return true;
    }

    @Override
    public Ordering getOrdering() {
        return Ordering.POLYNOMIAL;
    }

    // Don't care about ordering of one PolynomialValueNode w.r.t. another.
    //	@Override
    //	public int compareTo(Node other) {
    //		int i = super.compareTo(other);
    //		if (i != 0) {
    //			return i;
    //		}
    //		PolynomialValueNode n = (PolynomialValueNode) other;
    //		i = getPolynomialOrder() - n.getPolynomialOrder();
    //		if (i == 0) {
    //			i = getNumberOfVariables() - n.getNumberOfVariables();
    //		}
    //		if (i == 0) {
    //			i = Double.compare(getConstValue(), n.getConstValue());
    //		}
    //		for (int j = 0; i == 0 && j < getCoeff().length; j++) {
    //			i = getCoeff()[j].compareTo(n.getCoeff()[j]);
    //		}
    //		return i;
    //	}

    @Override
    public int getPolynomialOrder() {
        return coeff.length;
    }

    public int getNumberOfVariables() {
        return numberOfVariables;
    }

    /**
     * Get immutable coefficients.
     * @return polynomial coefficients
     */
    public PolynomialCoefficients[] getCoeff() {
        return coeff;
    }

    /**
     * Get mutable copy of coefficients.
     * @return polynomial coefficients
     */
    public PolynomialCoefficients[] getCopyOfCoeff() {
        PolynomialCoefficients[] a = new PolynomialCoefficients[coeff.length];
        for (int i = 0; i < coeff.length; i++) {
            a[i] = new PolynomialCoefficients(coeff[i]);
        }
        return a;
    }

    //	@Override
    //	public double evaluate(DataModel dataModel) {
    //		if (numberOfVariables != dataModel.getRealValues().length) {
    //			throw new MDPRuntimeException("Polynomial of " + numberOfVariables + " variables but data model has " + dataModel.getRealValues().length + "variables.");
    //		}
    //		double sum = getConstValue();
    //		for (PolynomialCoefficients c : coeff) {
    //			sum += c.evaluate(dataModel);
    //		}
    //		return sum;
    //	}
    //	
    @Override
    public Node apply(BinaryOperator binaryOperator, Node node) {
        return node.applyPolynomialValue(binaryOperator, this);
    }

    @Override
    public Node applyConstantValue(BinaryOperator binaryOperator, ConstantValueNode node) {
        return binaryOperator.executeConstantPolynomial(node, this);
    }

    @Override
    public Node applyLinearValue(BinaryOperator binaryOperator, LinearValueNode node) {
        return binaryOperator.executeLinearPolynomial(node, this);
    }

    @Override
    public Node applyPolynomialValue(BinaryOperator binaryOperator, PolynomialValueNode node) {
        return binaryOperator.executePolynomialPolynomial(node, this);
    }

    @Override
    public Node applyBooleanDecision(BinaryOperator binaryOperator, BooleanDecisionNode node) {
        return binaryOperator.executePolynomialBooleanDecision(this, node);
    }

    @Override
    public Node applyRealEqualDecision(BinaryOperator binaryOperator, RealEqualDecisionNode node) {
        return binaryOperator.executePolynomialRealEqualDecision(this, node);
    }

    @Override
    public Node applyRealGreaterOrEqualDecision(BinaryOperator binaryOperator, RealGreaterOrEqualDecisionNode node) {
        return binaryOperator.executePolynomialRealGreaterOrEqualDecision(this, node);
    }

    @Override
    public void appendTo(int depth, StringBuilder buf) {
        super.appendTo(depth, buf);
        buf.append("\n");
        for (PolynomialCoefficients c : coeff) {
            c.appendTo(depth, buf);
        }
    }


    // nested classes ...

    /**
     * Polynomial coefficients of a fixed order, for example the 2nd order terms
     * in a cubic.
     * TODO: Extract the multi-dimensional sparse array code into a SparseArray class.
     * TODO: Contains many methods to simplify code in BinaryOperator implementations.
     * Can we move these methods into the operators and reduce our method count
     * then extract an interface to use in the rest of the code. This would allow
     * us to experiment with alternative implementations.
     */
    public static class PolynomialCoefficients implements Comparable<PolynomialCoefficients> {

        private final TIntDoubleHashMap coeff;
        private final int coefficientOrder;
        private boolean immutable = false;

        /**
         * Create an empty set of coefficients of the given order.
         * @param coefficientOrder
         */
        public PolynomialCoefficients(int coefficientOrder) {
            this.coefficientOrder = coefficientOrder;
            coeff = new TIntDoubleHashMap();
        }

        public int compareTo(PolynomialCoefficients other) {
            // compare keys
            int[] keys = coeff.keys();
            int[] otherKeys = other.coeff.keys();
            int i = keys.length - otherKeys.length;
            if (i == 0) {
                Arrays.sort(keys);
                Arrays.sort(otherKeys);
                for (int j = 0; i == 0 && j < keys.length; j++) {
                    i = keys[j] - otherKeys[j];
                }
            }
            // if keys same, compare values
            for (int j = 0; i == 0 && j < keys.length; j++) {
                int key = keys[j];
                i = Double.compare(coeff.get(key), other.coeff.get(key));
            }
            return i;
        }

        /**
         * Create a mutable copy of a set of coefficients.
         * @param prototype
         */
        public PolynomialCoefficients(PolynomialCoefficients prototype) {
            coefficientOrder = prototype.coefficientOrder;
            coeff = (TIntDoubleHashMap) prototype.coeff.clone();
        }

        @Override
        public int hashCode() {
            final int prime = 31;
            int result = 1;
            result = prime * result + coeff.hashCode();
            result = prime * result + coefficientOrder;
            return result;
        }

        @Override
        public boolean equals(Object obj) {
            if (this == obj)
                return true;
            if (obj == null)
                return false;
            if (getClass() != obj.getClass())
                return false;
            PolynomialCoefficients other = (PolynomialCoefficients) obj;
            if (coeff == null) {
                if (other.coeff != null)
                    return false;
            } else if (!coeff.equals(other.coeff))
                return false;
            if (coefficientOrder != other.coefficientOrder)
                return false;
            return true;
        }

        @Override
        public String toString() {
            StringBuilder buf = new StringBuilder();
            appendTo(0, buf);
            return buf.toString();
        }

        public void appendTo(final int depth, final StringBuilder buf) {

            CoefficientProcedure printCoeff = new CoefficientProcedure() {

                private String delim = ": ";

                public void execute(int[] index, double value) {
                    buf.append(delim).append(value);
                    for (int i : index) {
                        buf.append(" x").append(i);
                    }
                    delim = " + ";
                }

            };

            Utils.appendIndent(depth, buf);
            buf.append("coeff of order ").append(coefficientOrder);
            forEachEntry(printCoeff);
            buf.append('\n');
        };

        public void setImmutable() {
            this.immutable = true;
        }

        public void setCoefficient(int[] index, double value) {
            if (immutable) {
                throw new XADDRuntimeException("Immutable");
            }
            if (index.length != coefficientOrder) {
                throw new XADDRuntimeException("Got " + index.length + " indices, but expected " + coefficientOrder);
            }
            coeff.put(Utils.indexToKey(index), value);
        }

        public double getCoefficient(int[] index) {
            if (index.length != coefficientOrder) {
                throw new XADDRuntimeException("Got " + index.length + " indices, but expected " + coefficientOrder);
            }
            return coeff.get(Utils.indexToKey(index));
        }

        /**
         * Add coefficients from other into our own values.
         * @param other
         */
        public void addCoefficients(PolynomialCoefficients other) {
            if (immutable) {
                throw new XADDRuntimeException("Immutable");
            }
            for (TIntDoubleIterator iter = other.coeff.iterator(); iter.hasNext(); ) {
                iter.advance();
                coeff.adjustOrPutValue(iter.key(), iter.value(), iter.value());
            }
        }

        /**
         * Multiply coefficients by constValue.
         * @param constValue
         */
        public void multiplyCoefficients(double constValue) {
            if (immutable) {
                throw new XADDRuntimeException("Immutable");
            }
            for (TIntDoubleIterator iter = coeff.iterator(); iter.hasNext(); ) {
                iter.advance();
                iter.setValue(iter.value() * constValue);
            }
        }

        /**
         * Add product into our coefficients.
         * E.g. values below will add constValue to our term [ 0, 0, 0, 0, 3 ] -> (x0 ^4).x3
         * @param constValue
         * @param index1 specifies a term e.g. [ 0, 0 ] -> x0.x0
         * @param index2 specifies a term e.g. [ 0, 0, 3 ] -> x0.x0.x3
         */
        public void accumulateProduct(double constValue, int[] index1, int[] index2) {
            if (immutable) {
                throw new XADDRuntimeException("Immutable");
            }
            int[] index = Arrays.copyOf(index1, index1.length + index2.length);
            if (index.length != coefficientOrder) {
                throw new XADDRuntimeException("Can't add product of order " + index1.length + " term and order " + index2.length + " term into order " + coefficientOrder + " coefficients.");
            }
            System.arraycopy(index2, 0, index, index1.length, index2.length);
            Arrays.sort(index);
            coeff.adjustOrPutValue(Utils.indexToKey(index), constValue, constValue);
        }

        /**
         * Add product into our coefficients.
         * @param constValue
         * @param other
         */
        public void accumulateProduct(double constValue, int key, int order) {
            if (immutable) {
                throw new XADDRuntimeException("Immutable");
            }
            if (order != coefficientOrder) {
                throw new XADDRuntimeException("Can't add product of constant and order " + order + " term into order " + coefficientOrder + " coefficients.");
            }
            coeff.adjustOrPutValue(key, constValue, constValue);
        }

        /**
         * Visit each coefficient with procedure.
         * @param procedure
         */
        public boolean forEachEntry(TIntDoubleProcedure procedure) {
            return coeff.forEachEntry(procedure);
        }

        /**
         * Visit each coefficient with procedure.
         * @param procedure
         */
        public void forEachEntry(CoefficientProcedure procedure) {
            coeff.forEachEntry(getTIntDoubleProcedure(procedure));
        }

        private TIntDoubleProcedure getTIntDoubleProcedure(final CoefficientProcedure procedure) {
            return new TIntDoubleProcedure() {
                private int[] index = new int[coefficientOrder];

                public boolean execute(int key, double value) {
                    Utils.keyToIndex(key, index); // sets index
                    procedure.execute(index, value);
                    return true; // continue iteration
                }

            };
        }

        //		/**
        //		 * Sum all terms for our coefficientOrder.
        //		 * @param dataModel
        //		 * @return sum of terms
        //		 */
        //		public double evaluate(DataModel dataModel) {
        //			SumProceedure sumProcedure = new SumProceedure(dataModel);
        //			forEachEntry(sumProcedure);
        //			return sumProcedure.getSum();
        //		}

    }

    public interface CoefficientProcedure {
        void execute(int[] index, double value);
    }

    //	/**
    //	 * Sum all terms for our coefficientOrder.
    //	 */
    //	public static class SumProceedure implements CoefficientProcedure {
    //		private final DataModel dataModel;
    //		private double sum = 0.0;
    //		
    //		public SumProceedure(DataModel dataModel) {
    //			this.dataModel = dataModel;
    //		}
    //		
    //		public void execute(int[] index, double value) {
    //			double x = value;
    //			for (int i : index) {
    //				x *= dataModel.getReal(i);
    //			}
    //			log.debug("value = " + value + ", vars = " + Arrays.toString(dataModel.getRealValues()) + ", index = " + Arrays.toString(index) + ", x = " + x);
    //			sum += x;
    //		}
    //
    //		public double getSum() {
    //			return sum;
    //		}
    //
    //	}

}
